package cn.wapache.time.calendar;

import java.text.ParseException;
import java.util.Date;
import java.util.TimeZone;

/**
 * This implementation of the Calendar excludes the set of times expressed by a
 * given {@link cn.wapache.time.calendar.CronExpression CronExpression}. For example, you 
 * could use this calendar to exclude all but business hours (8AM - 5PM) every 
 * day using the expression &quot;* * 0-7,18-23 ? * *&quot;. 
 * <P>
 * It is important to remember that the cron expression here describes a set of
 * times to be <I>excluded</I> from firing. Whereas the cron expression in 
 * {@link cn.wapache.trigger.quartz.api.trigger.CronTrigger CronTrigger} describes a set of times that can
 * be <I>included</I> for firing. Thus, if a <CODE>CronTrigger</CODE> has a 
 * given cron expression and is associated with a <CODE>CronCalendar</CODE> with
 * the <I>same</I> expression, the calendar will exclude all the times the 
 * trigger includes, and they will cancel each other out. 
 * 
 * @author Aaron Craven
 */
public class CronCalendar extends BaseCalendar {
    static final long serialVersionUID = -8172103999750856831L;

    CronExpression cronExpression;

    public CronCalendar(String expression) throws ParseException {
        this(null, expression, null);
    }

    public CronCalendar(Calendar baseCalendar, String expression) 
    		throws ParseException {
        this(baseCalendar, expression, null);
    }

    public CronCalendar(Calendar baseCalendar, String expression, 
    		TimeZone timeZone) throws ParseException {
        super(baseCalendar);
        this.cronExpression = new CronExpression(expression);
        this.cronExpression.setTimeZone(timeZone);
    }
    
    @Override
    public Object clone() {
        CronCalendar clone = (CronCalendar) super.clone();
        clone.cronExpression = new CronExpression(cronExpression);
        return clone;
    }

    @Override
    public TimeZone getTimeZone() {
        return cronExpression.getTimeZone();
    }

    @Override
    public void setTimeZone(TimeZone timeZone) {
        cronExpression.setTimeZone(timeZone);
    }

    @Override
    public boolean isTimeIncluded(long timeInMillis) {        
        if ((getBaseCalendar() != null) && 
                (getBaseCalendar().isTimeIncluded(timeInMillis) == false)) {
            return false;
        }
        
        return (!(cronExpression.isSatisfiedBy(new Date(timeInMillis))));
    }

    @Override
    public long getNextIncludedTime(long timeInMillis) {
        long nextIncludedTime = timeInMillis + 1; //plus on millisecond
        
        while (!isTimeIncluded(nextIncludedTime)) {

            //If the time is in a range excluded by this calendar, we can
            // move to the end of the excluded time range and continue testing
            // from there. Otherwise, if nextIncludedTime is excluded by the
            // baseCalendar, ask it the next time it includes and begin testing
            // from there. Failing this, add one millisecond and continue
            // testing.
            if (cronExpression.isSatisfiedBy(new Date(nextIncludedTime))) {
                nextIncludedTime = cronExpression.getNextInvalidTimeAfter(
                        new Date(nextIncludedTime)).getTime();
            } else if ((getBaseCalendar() != null) && 
                    (!getBaseCalendar().isTimeIncluded(nextIncludedTime))){
                nextIncludedTime = 
                    getBaseCalendar().getNextIncludedTime(nextIncludedTime);
            } else {
                nextIncludedTime++;
            }
        }
        
        return nextIncludedTime;
    }

    @Override
    public String toString() {
    	return new StringBuilder("base calendar: [")
        .append(getBaseCalendar() == null ? "null" : getBaseCalendar())
        .append("], excluded cron expression: '")
        .append(cronExpression)
        .append("'")
        .toString();
    }
    
    public CronExpression getCronExpression() {
        return cronExpression;
    }
    
    public void setCronExpression(String expression) throws ParseException {
    	cronExpression = new CronExpression(expression);
    }

    public void setCronExpression(CronExpression expression) {
        if (expression == null) {
            throw new IllegalArgumentException("expression cannot be null");
        }
        this.cronExpression = expression;
    }
}